package jp.dodododo.janerics;

import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

import jp.dodododo.janerics.exception.UnsupportedTypeException;

/**
 * {@.en This class contains various thin wrapper methods for around the Java API.}
 * 
 * <br />
 * 
 * {@.ja Java APIの薄いラッパーメソッドを提供します。}
 * 
 * @author Satoshi Kimura
 */
public abstract class GenericsTypeUtil {

	/**
	 * {@.en Returns the Type of element.(Return type parameter.)}
	 * 
	 * <br />
	 * 
	 * {@.ja Collectionの要素の型(型パラメータ)を返します。}
	 * 
	 * <pre>
	 * Type returnType = ... // returnType is List&lt;String&gt;
	 * Type type = getElementTypeOfCollection(returnType); // return String.class
	 * </pre>
	 * 
	 * @param type
	 *            {@.en type of collection} {@.ja コレクション型}
	 * @return {@.en type of element} {@.ja 要素の型}
	 * 
	 */
	public static Type getElementTypeOfCollection(Type type) {
		if (isTypeOf(type, Collection.class) == false) {
			throw new UnsupportedTypeException(type);
		}
		return GenericsTypeUtil.getTypeParameter(type, 0);
	}

	/**
	 * {@.en Returns the key type of Map. }
	 * 
	 * <br />
	 * 
	 * {@.ja マップのキーの型を返します。}
	 * 
	 * @param type
	 *            {@.en Type of map} {@.ja マップ型}
	 * @return {@.en Key type of map} {@.ja マップのキーの型}
	 */
	public static Type getKeyTypeOfMap(Type type) {
		if (isTypeOf(type, Map.class) == false) {
			throw new UnsupportedTypeException(type);
		}
		return GenericsTypeUtil.getTypeParameter(type, 0);
	}

	/**
	 * {@.en Returns the value type of Map. }
	 * 
	 * <br />
	 * 
	 * {@.ja マップのバリューの型を返します。}
	 * 
	 * @param type
	 *            {@.en Type of map} {@.ja マップ型}
	 * @return {@.en Value type of map} {@.ja マップのバリューの型}
	 */
	public static Type getValueTypeOfMap(Type type) {
		if (isTypeOf(type, Map.class) == false) {
			throw new UnsupportedTypeException(type);
		}
		return GenericsTypeUtil.getTypeParameter(type, 1);
	}

	/**
	 * {@.en Returns "type" is type of "clazz". }
	 * 
	 * <br />
	 * 
	 * {@.ja typeがclazzの型か返します。}
	 * 
	 * @param type
	 *            {@.en type} {@.ja type}
	 * @param clazz
	 *            {@.en clazz} {@.ja clazz}
	 * @return {@.en } {@.ja }
	 */
	public static boolean isTypeOf(Type type, Class<?> clazz) {
		if (Class.class.isInstance(type)) {
			return clazz.isAssignableFrom(Class.class.cast(type));
		}
		if (ParameterizedType.class.isInstance(type)) {
			ParameterizedType parameterizedType = ParameterizedType.class.cast(type);
			return isTypeOf(parameterizedType.getRawType(), clazz);
		}
		return false;
	}

	/**
	 * {@.en Returns the actual class of "type". }
	 * 
	 * <br />
	 * 
	 * {@.ja typeの実際のクラスを返します。}
	 * 
	 * @param type
	 *            {@.en type} {@.ja type}
	 * @param targetClass
	 *            {@.en targetClass} {@.ja targetClass}
	 * @return {@.en The actual class of "type"} {@.ja typeの実際のクラス}
	 */
	public static Class<?> getActualClass(Type type, Class<?> targetClass) {
		Map<TypeVariable<?>, Type> map = getTypeVariableMap(targetClass);
		return getActualClass(type, map);
	}

	/**
	 * {@.en Returns the actual type of "type". }
	 * 
	 * <br />
	 * 
	 * {@.ja typeの実際の型を返します。}
	 * 
	 * @param type
	 *            {@.en type} {@.ja type}
	 * @param targetClass
	 *            {@.en targetClass} {@.ja targetClass}
	 * @return {@.en the actual type of "type"} {@.ja typeの実際の型}
	 */
	public static Type getActualType(Type type, Class<?> targetClass) {
		Map<TypeVariable<?>, Type> map = getTypeVariableMap(targetClass);
		return getActualType(type, map);
	}

	private static Class<?> getActualClass(Type type, Map<TypeVariable<?>, Type> map) {
		if (Class.class.isInstance(type)) {
			return Class.class.cast(type);
		}
		if (ParameterizedType.class.isInstance(type)) {
			return getActualClass(getActualType(type, map), map);
		}
		if (TypeVariable.class.isInstance(type)) {
			return getActualClass(getActualType(type, map), map);
		}
		if (GenericArrayType.class.isInstance(type)) {
			GenericArrayType genericArrayType = GenericArrayType.class.cast(type);
			Class<?> componentClass = getActualClass(genericArrayType.getGenericComponentType(), map);
			return Array.newInstance(componentClass, 0).getClass();
		}
		return null;
	}

	private static Type getActualType(Type type, Map<TypeVariable<?>, Type> map) {
		if (Class.class.isInstance(type)) {
			return type;
		}
		if (ParameterizedType.class.isInstance(type)) {
			return ParameterizedType.class.cast(type).getRawType();
		}
		if (TypeVariable.class.isInstance(type)) {
			return map.get(TypeVariable.class.cast(type));
		}
		return null;
	}

	/**
	 * {@.en Returns the raw class of "type". }
	 * 
	 * <br />
	 * 
	 * {@.ja typeの原型のクラスを返します。}
	 * 
	 * @param type
	 *            @.en type} {@.ja type}
	 * @return {@.en The raw class of "type"} {@.ja typeの原型のクラス}
	 */
	public static Class<?> getRawClass(Type type) {
		if (Class.class.isInstance(type)) {
			return Class.class.cast(type);
		}
		if (ParameterizedType.class.isInstance(type)) {
			ParameterizedType parameterizedType = ParameterizedType.class.cast(type);
			return getRawClass(parameterizedType.getRawType());
		}
		if (GenericArrayType.class.isInstance(type)) {
			GenericArrayType genericArrayType = GenericArrayType.class.cast(type);
			Class<?> rawClass = getRawClass(genericArrayType.getGenericComponentType());
			return Array.newInstance(rawClass, 0).getClass();
		}
		return null;
	}

	/**
	 * {@.en Returns the type parameter of "type". }
	 * 
	 * <br />
	 * 
	 * {@.ja typeの型引数を返します。}
	 * 
	 * @param type
	 *            {@.en type} {@.ja タイプ}
	 * @param index
	 *            {@.en index of parameter} {@.ja パラメータのインデックス}
	 * @return {@.en the type parameter.} {@.ja typeの型引数。}
	 */
	public static Type getTypeParameter(Type type, int index) {
		Type[] genericParameters = getTypeParameters(type);
		if (genericParameters == null) {
			return null;
		}
		return genericParameters[index];
	}

	/**
	 * {@.en Returns the type parameters of "type". }
	 * 
	 * <br />
	 * 
	 * {@.ja typeの型引数を返します。}
	 * 
	 * @param type
	 *            {@.en type} {@.ja タイプ}
	 * @return {@.en the type parameters.If type is not parameterized, new Type[0]} {@.ja typeの型引数。パラメータ化された型でない場合はnew Type[0]}
	 */
	public static Type[] getTypeParameters(Type type) {
		if (ParameterizedType.class.isInstance(type)) {
			return ParameterizedType.class.cast(type).getActualTypeArguments();
		}
		if (GenericArrayType.class.isInstance(type)) {
			return getTypeParameters(GenericArrayType.class.cast(type).getGenericComponentType());
		}
		if (Class.class.isInstance(type)) {
			return Class.class.cast(type).getTypeParameters();
		}
		return new Type[0];
	}

	private static Map<TypeVariable<?>, Type> getTypeVariableMap(final Class<?> clazz) {
		final Map<TypeVariable<?>, Type> map = new LinkedHashMap<TypeVariable<?>, Type>();

		if (clazz == null) {
			return map;
		}

		final Class<?> superClass = clazz.getSuperclass();
		final Type superClassType = clazz.getGenericSuperclass();
		if (superClass != null) {
			gatherTypeVariables(superClass, superClassType, map);
		}

		final Class<?>[] interfaces = clazz.getInterfaces();
		final Type[] interfaceTypes = clazz.getGenericInterfaces();
		for (int i = 0; i < interfaces.length; ++i) {
			gatherTypeVariables(interfaces[i], interfaceTypes[i], map);
		}

		return map;
	}

	private static void gatherTypeVariables(final Class<?> clazz, final Type type, final Map<TypeVariable<?>, Type> map) {
		if (clazz == null) {
			return;
		}
		gatherTypeVariables(type, map);

		final Class<?> superClass = clazz.getSuperclass();
		final Type superClassType = clazz.getGenericSuperclass();
		if (superClass != null) {
			gatherTypeVariables(superClass, superClassType, map);
		}

		final Class<?>[] interfaces = clazz.getInterfaces();
		final Type[] interfaceTypes = clazz.getGenericInterfaces();
		for (int i = 0; i < interfaces.length; ++i) {
			gatherTypeVariables(interfaces[i], interfaceTypes[i], map);
		}
	}

	private static void gatherTypeVariables(final Type type, final Map<TypeVariable<?>, Type> map) {
		if (ParameterizedType.class.isInstance(type)) {
			final ParameterizedType parameterizedType = ParameterizedType.class.cast(type);
			final TypeVariable<?>[] typeVariables = GenericDeclaration.class.cast(parameterizedType.getRawType()).getTypeParameters();
			final Type[] actualTypes = parameterizedType.getActualTypeArguments();
			for (int i = 0; i < actualTypes.length; ++i) {
				map.put(typeVariables[i], actualTypes[i]);
			}
		}
	}
}
